Skip to content

chore: sync Telegram successor with current dev - #26

Merged
twoimo merged 15 commits into
successor/telegram-tool-activity-current-v2from
sync/telegram-dev-41d6850
Jul 26, 2026
Merged

chore: sync Telegram successor with current dev#26
twoimo merged 15 commits into
successor/telegram-tool-activity-current-v2from
sync/telegram-dev-41d6850

Conversation

@twoimo

@twoimo twoimo commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Internal synchronization PR. Merge current upstream dev at 41d6850c into the preserved Telegram tool-activity successor before applying the remaining changelog fix and running exact-head verification.

Yeachan-Heo and others added 15 commits July 25, 2026 23:59
…ueue availability seams (Yeachan-Heo#3159)

Yeachan-Heo#3073 widened two availability predicates in InputController#isActionAvailable:

- app.message.queue now also requires a non-empty composer draft, reading
  this.ctx.editor.getText().
- app.message.dequeue now reads this.ctx.session.getQueuedMessageEntries()
  and this.ctx.compactionQueuedMessages instead of session.queuedMessageCount.

The g002/g003 red-team contexts are hand-built partial InteractiveModeContext
stubs that predate those reads, so the predicates threw. ActionRegistry catches
availability throws, reports via showError and caches false, which surfaced as:

- g002:380 app.message.queue availability false (editor stub absent)
- g003:171 palette order gaining a leading "error" entry, because building the
  palette evaluates availability for every action and the dequeue throw fired
  showError before the hide/focus/execute sequence.

Both are stub gaps, not product regressions: the same code and the same tests
pass at the PR head ce4f799, and the failure only appears once Yeachan-Heo#3073 landed
alongside these files. Verified the product seams behave correctly by asserting
draft gating (empty/whitespace/non-empty) and dequeue truth from real session
plus compaction entries, with zero swallowed availability errors.

Fix is test-only, 3 lines, filling the missing context surfaces.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…xDelayMs (Yeachan-Heo#3156)

* fix(coding-agent): cap legacy auto-compaction Retry-After at retry.maxDelayMs

The auto-compaction retry loop recovered Retry-After by regex over provider
error prose (`#parseRetryAfterMsFromError`) and then slept on it uncapped:

    const baseDelayMs = retrySettings.baseDelayMs * 2 ** attempt;
    const delayMs = retryAfterMs !== undefined ? Math.max(baseDelayMs, retryAfterMs) : baseDelayMs;

A 30s heuristic switches to the next compaction candidate when the delay is
too long, but it explicitly falls through on the last candidate ("No more
candidates - we have to wait"). There the session slept for the full
server-suggested duration — a 3h hint produced a 3h sleep.

This contradicts the documented legacy rule in non-compaction-retry-policy.md:

  "retry.maxDelayMs caps every legacy session retry delay, including provider
   retry-after hints. Managed fallback intentionally does not cap typed
   Retry-After values because it retries within its separate per-entry budget."

Compaction is legacy (prose-parsed, not typed transport facts) and has no
per-entry budget, so it must be capped. The non-compaction legacy path already
does exactly this at agent-session.ts (`Math.min(retryAfterMs, maxDelayMs)`);
compaction was the only legacy consumer missing the bound.

Adds `compactionRetryDelay()` next to `cappedExponentialWithFullJitter` and
`effectiveFallbackDelay`, following this file's existing convention of pure,
directly unit-tested delay math. It also collapses a missing/NaN/infinite hint
to "no usable hint" — `#parseRetryAfterMsFromError` can return Infinity via
`Number("1e999") * 1000`.

Managed fallback behaviour is deliberately untouched: `effectiveFallbackDelay`
stays uncapped and its "intentionally uncapped" contract test still passes.
The two tests now sit adjacent and document the two-tier policy as a pair.

Candidate-switching is unaffected in the default configuration: a hint above
the cap still exceeds the 30s threshold either way, so only the terminal
last-candidate sleep changes (3h -> retry.maxDelayMs, default 5min).

Gates: routing-adversarial (9 pass), fallback/retry/compaction suites
(279 pass across 34 files), tsc --noEmit clean. The 2 failures in
agent-session-retry-fallback.test.ts are pre-existing on origin/dev
(identical 14 pass / 2 fail with these changes stashed).

* test(coding-agent): strengthen compaction retry-after cap coverage

Adds two contract probes on top of the existing cap regressions:

- Cross-path parity: both legacy surfaces (auto-compaction and the
  non-compaction session retry) recover Retry-After from provider prose, so
  the documented rule — "retry.maxDelayMs caps every legacy session retry
  delay, including provider retry-after hints" — must bind them identically.
  The probe pins compaction to the legacy non-compaction bound
  (Math.min(retryAfterMs, maxDelayMs)) so a future change to one cannot
  silently drift from the other.

- Exhaustive invariant grid: 700 combinations of
  (baseDelayMs, maxDelayMs, attempt, retryAfterMs) — including NaN, +Infinity,
  negative, and undefined hints, and the maxDelayMs <= 0 "no cap" convention —
  assert the delay is always finite, non-negative, and within the cap.

Non-vacuity verified by mutation: reverting the cap to the pre-fix
`return hinted` fails 3 of the compaction probes (previously 1).
expect() calls in this file go 64 -> 2002.

No production change; managed fallback stays intentionally uncapped and its
contract test is untouched.

---------

Co-authored-by: dmae97 <dmae97@users.noreply.github.com>
…gnal (Yeachan-Heo#3076)

Workflow-state readers (readJsonFile/readJsonValue) and the handoff paths
wrote raw `WARNING: ... ignoring corrupt state` bytes to process.stderr.
During an interactive session those bytes land in the TUI alternate-screen
stream and paint over the live composer (Yeachan-Heo#3002).

Route every such warning through the TUI-safe centralized file logger
(console transport off by default) via the `@gajae-code/utils/logger`
subpath, which keeps this module native-free for the gjc-state-gates shards
(the package barrel pulls procmgr/ptree -> @gajae-code/natives).

Unlike the file-logger-only approach, corrupt read/status/handoff results
still surface the warning on the structured StateCommandResult.stderr channel,
so `gjc state` CLI/automation can still tell corrupt state from absent state.
The readers take an optional onWarning sink: in-process/TUI callers pass none
(logger only), while handleRead/handleStatus collect it into the command
result.

Regressions: corrupt read/status now assert (a) no raw process.stderr write,
(b) the warning present on the command result, (c) logger.warn carried it; plus
an in-process readWorkflowStateJson path that must stay off process.stderr.

Builds on Yeachan-Heo#3042 by @innocarpe (reviewed MERGE_READY, closed only for the
read/status command-result gap this change closes); fixes Yeachan-Heo#3002.
… e2e (Yeachan-Heo#3160)

Post-Yeachan-Heo#3076 shard-6 red was EBADF on closeSync in runCli finally after the
fail-closed lifecycle idempotency path, not a product contract failure.
Ignore already-closed capture FDs so the CLI exit/JSON assertions remain
visible under CI load.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…icit keyhints (Yeachan-Heo#3161)

Co-authored-by: twoimo <twoimo@twoimoui-MacBookPro.local>
…re rounds (Yeachan-Heo#3162)

* fix(deep-interview): unblock apply-round-result so interviews can score rounds

`gjc deep-interview apply-round-result` failed with DI_INTERNAL_ERROR on
every call, so a deep interview could never score a single round. Three
independent defects stacked on the same path:

1. The Round-0 topology gate is persisted by the recorder as an
   `answered` round shell (the locked intent contract binds to its answer
   hash), but it can never be scored: `apply-round-result` validates
   `--round` as positive. `applyDeepInterviewRoundResultV1` still counted
   it in the "every strictly-earlier round must be scored" precondition,
   permanently deadlocking every later round.
2. `decodeDeepInterviewRoundResultJson` materializes every optional key,
   so an omitted `targeting`/`ontology`/`bookkeeping` arrives as an
   explicit `undefined`. `canonicalJsonValue` rejected `undefined`
   outright, so computing the replay digest threw a raw TypeError that
   surfaced as the opaque DI_INTERNAL_ERROR.
3. `scoreToUnits` tested the raw float product. `0.69 * 10_000` is
   `6900.000000000001` and effective ambiguity round-trips through
   `units / 10_000`, so ordinary scores were rejected as non-integral
   1e-4 units.

Fixes, in order: exclude non-scorable Round-0 gate shells from the
ordering precondition; drop `undefined` object properties in canonical
JSON exactly like `JSON.stringify` (array elements and the top-level
value stay strict, and absent vs present-but-undefined now digest
identically); decide 1e-4 units from the shortest round-trip decimal
instead of an epsilon, which accepts every genuine four-decimal score
while still rejecting off-grid precision such as `0.00005` and
`0.05000000000000001`.

Tests: an end-to-end regression that records a Round-0 topology gate
through the recorder and then scores Round 1 through the repair CLI, plus
unit coverage for canonical-JSON undefined handling, digest equivalence,
and the 1e-4 unit boundaries.

* test(deep-interview): pin the invariants the scoring fix relies on

Independent architect review flagged two coverage gaps and three nits;
all are addressed here.

- Pin the premise behind the Round-0 exclusion, which is sound only
  because a round-0 record can never carry scoring. A first attempt
  asserted this with a fixture that was rejected for four unrelated
  reasons and stayed green when the rule was deleted; it is now a
  positive control plus a negative case differing by exactly one field
  (`scores`), so deleting the validator's round-0 clause turns it red.
  Also assert `apply-round-result --round 0` returns DI_INVALID_ROUND.
- Pin the collision boundary that makes dropping `undefined` safe: null
  (and 0/""/false) must survive canonical JSON. Loosening the skip
  predicate to `!value[key]` would otherwise change the meaning of every
  persisted round_result_digest with nothing failing.
- Replace the hand-picked scoreToUnits spot checks with an exhaustive
  sweep over all 10,001 grid values, plus -0, 0.9999, and
  0.30000000000000004.
- Cover CLI idempotency for a result JSON that omits optional keys - the
  exact shape that used to throw - asserting the replay settles as a
  noop (status 0, written:false) rather than a second write.
- Normalize `question_hash` with `?? null` in
  deepInterviewAnswerIdentityEqual, the one compared field left raw. For
  any record that has a question_hash this is the identity; it only makes
  an omitted and an explicitly-null hash compare equal, matching all
  seven sibling fields. No reachable behavior change.
- State the Round-0 exclusion as `round.round !== 0` (identity) instead of
  `>= 1` (a range that also silently covered unreachable negatives), and
  drop the now-unreachable `Number.isSafeInteger` throw in scoreToUnits:
  the [0,1] guard plus the four-digit cap already bound units to an
  integer in [0, 10_000].

Each of the four assertions was mutation-tested by reverting the
corresponding production change and confirming the suite fails.
…Heo#3171)

The stream-watchdog idle-timeout helpers (getStreamIdleTimeoutMs,
getOpenAIStreamIdleTimeoutMs) only read the PI_-prefixed env names, so the
documented GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS (docs/environment-variables.md)
was a silent no-op. Resolve it GJC-first with the PI_ names as legacy
fallbacks, matching the migration in Yeachan-Heo#2827/Yeachan-Heo#2943/Yeachan-Heo#3041. Semantics preserved
(?? nullish chain; =0 still disables the watchdog).

Extends stream-timeout-defaults.test.ts with GJC-first, override, disable, and
alias-fallback coverage for both getters.
…eo#3165) (Yeachan-Heo#3170)

* fix(coding-agent): enforce ralplan consensus iteration cap (Yeachan-Heo#3165)

Native `gjc ralplan --write` now refuses planner/revision openers past a
finite consensus-iteration budget (default 5, override via
gjc.ralplan.maxIterations). Overflow exits 3 with PLANNING-STUCK so
headless runs cannot silently loop on perpetual ITERATE, while final
escalation remains allowed without auto-implementation.

* fix(coding-agent): fail closed ralplan cap on untrusted index (Yeachan-Heo#3165)

Floor consensus opener count by on-disk stage-*-{planner,revision}.md so a
wiped, truncated, deleted, or malformed index.jsonl cannot under-count and
allow unbounded revision openers after prior passes.

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
@twoimo
twoimo merged commit 7b1e4d4 into successor/telegram-tool-activity-current-v2 Jul 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants